package org.codefilarete.stalactite.engine.configurer.builder;

import java.util.Map;

import org.codefilarete.reflection.AccessorDefinition;
import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration;
import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration.KeyMapping;
import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration.SingleKeyMapping;
import org.codefilarete.stalactite.dsl.idpolicy.GeneratedKeysPolicy;
import org.codefilarete.stalactite.dsl.key.CompositeKeyMappingConfiguration;
import org.codefilarete.stalactite.dsl.naming.ColumnNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.UniqueConstraintNamingStrategy;
import org.codefilarete.stalactite.engine.configurer.AbstractIdentification;
import org.codefilarete.stalactite.engine.configurer.AbstractIdentification.CompositeKeyIdentification;
import org.codefilarete.stalactite.engine.configurer.AbstractIdentification.SingleColumnIdentification;
import org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableMapping;
import org.codefilarete.stalactite.engine.configurer.builder.embeddable.EmbeddableMappingBuilder;
import org.codefilarete.stalactite.engine.configurer.entity.CompositeKeyLinkageSupport;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.PrimaryKey;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.statement.binder.ColumnBinderRegistry;
import org.codefilarete.tool.VisibleForTesting;

import static org.codefilarete.tool.Nullable.nullable;

public class PrimaryKeyStep<C, I> {
	
	/**
	 * Creates primary key on table owning identification
	 *
	 * @param identification information that allow to create primary key
	 * @return the created {@link PrimaryKey}
	 */
	@VisibleForTesting
	<T extends Table<T>> PrimaryKey<T, I> addIdentifyingPrimarykey(AbstractIdentification<C, I> identification,
																   Map<EntityMappingConfiguration, Table> tableMap,
																   ColumnBinderRegistry columnBinderRegistry,
																   ColumnNamingStrategy columnNamingStrategy,
																   UniqueConstraintNamingStrategy uniqueConstraintNamingStrategy) {
		T pkTable = (T) tableMap.get(identification.getIdentificationDefiner());
		KeyMapping<C, I> keyLinkage = identification.getKeyLinkage();
		AccessorDefinition identifierDefinition = AccessorDefinition.giveDefinition(identification.getIdAccessor());
		if (identification instanceof AbstractIdentification.CompositeKeyIdentification) {
			CompositeKeyMappingConfiguration<I> configuration = ((CompositeKeyLinkageSupport<C, I>) keyLinkage).getCompositeKeyMappingBuilder().getConfiguration();
			// Note that we won't care about readonly column returned by build(..) since we're on a primary key case, then
			// some readonly columns would be nonsense
			EmbeddableMappingBuilder<I, T> compositeKeyBuilder = new EmbeddableMappingBuilder<>(configuration, pkTable, columnBinderRegistry, columnNamingStrategy, uniqueConstraintNamingStrategy);
			EmbeddableMapping<I, T> build = compositeKeyBuilder.build();
			Map<ReadWritePropertyAccessPoint<I, Object>, Column<T, Object>> compositeKeyMapping = build.getMapping();
			compositeKeyMapping.values().forEach(Column::primaryKey);
			((CompositeKeyIdentification<C, I>) identification).setCompositeKeyMapping(compositeKeyMapping);
		} else {
			String columnName = determineColumnName((SingleKeyMapping) keyLinkage, columnNamingStrategy);
			// nullability = false may not be necessary because of primary key, let for principle
			Column<?, I> primaryKey = pkTable.addColumn(columnName, identifierDefinition.getMemberType(), ((SingleKeyMapping<C, I>) keyLinkage).getColumnOptions().getColumnSize(), false);
			primaryKey.primaryKey();
			if (((SingleColumnIdentification<C, I>) identification).getIdentifierPolicy() instanceof GeneratedKeysPolicy) {
				primaryKey.autoGenerated();
			}
		}
		return pkTable.getPrimaryKey();
	}

	private <O> String determineColumnName(SingleKeyMapping<C, O> keyLinkage, ColumnNamingStrategy columnNamingStrategy) {
		return nullable(keyLinkage.getColumnOptions().getColumnName())
				.elseSet(keyLinkage::getFieldName)
				.elseSet(() -> columnNamingStrategy.giveName(AccessorDefinition.giveDefinition(keyLinkage.getAccessor())))
				.get();
	}
}
